#include <dirent.h>
#include <dlfcn.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <uuid/uuid.h>

#include <stdio.h>

#define NIT_SHORT_NAMES
#include <nit/macros.h>
#include <nit/palloc.h>
#include <nit/list.h>
#include <nit/hset.h>
#include <nit/hmap.h>

#include "bbu.h"

#define NEW_FD_FLAGS O_WRONLY | O_CREAT | O_TRUNC
#define NEW_FD_MODE S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH
#define DIR_MODE S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH

typedef char Path_str[sizeof('/') + 36];

static void
id_gen(uuid_t des, uuid_t src)
{
	if (src)
		uuid_copy(des, src);
	else
		uuid_generate(des);
}

Bbu_expr *
bbu_expr_var(Bbu_vid var)
{
	Bbu_expr *expr = palloc(expr);

	pcheck(expr, NULL);
	expr->type = BBU_VAR;
	id_gen(expr->i.var, var);
	return expr;
}

Bbu_expr *
bbu_expr_fa(Bbu_uid f_id, Bbu_elist *args)
{
	Bbu_expr *expr = palloc(expr);

	pcheck(expr, NULL);
	expr->type = BBU_FA;
	uuid_copy(expr->i.fa.f_id, f_id);
	expr->i.fa.args = args;
	return expr;
}

Bbu_expr *
bbu_expr_blob(void *bytes, uint64_t size)
{
	Bbu_expr *expr = palloc(expr);

	pcheck(expr, NULL);
	expr->type = BBU_BLOB;
	expr->i.blob.size = size;
	expr->i.blob.bytes = bytes;
	return expr;
}

Bbu_expr *
bbu_expr_blob_copy(const void *bytes, uint64_t size)
{
	void *bytes_cpy = malloc(size);
	Bbu_expr *expr;

	pcheck(bytes_cpy, NULL);
	memcpy(bytes_cpy, bytes, size);

        if (!(expr = bbu_expr_blob(bytes_cpy, size)))
		free(bytes_cpy);

	return expr;
}

void
bbu_expr_free(Bbu_expr *expr)
{
	switch (expr->type) {
	case BBU_VAR:
		break;
	case BBU_FA:
		bbu_elist_free_all(expr->i.fa.args);
		break;
	case BBU_BLOB:
		free(expr->i.blob.bytes);
		break;
	}

	free(expr);
}

Bbu_elist *
bbu_elist_new(Bbu_expr *expr, Bbu_elist *next)
{
	Bbu_elist *list = palloc(list);

	pcheck(list, NULL);
	list->expr = expr;
	LIST_APP(list, next);
	return list;
}

Bbu_elist *
bbu_nest(Bbu_expr *expr)
{
	return bbu_elist_new(expr, NULL);
}

Bbu_elist *
bbu_elist_va(Bbu_expr *first, ...)
{
	Bbu_elist *head = NULL;
	Bbu_elist **end = &head;
	Bbu_expr *e = first;
	va_list vl;

	va_start(vl, first);

	for (; e; e = va_arg(vl, void *), end = NEXT_REF(*end, Bbu_elist))
	        if (!(*end = bbu_elist_new(e, NULL))) {
			delayed_foreach (head)
				free(head);

			va_end(vl);
			return NULL;
		}

	va_end(vl);
	return head;
}

void
bbu_elist_free(Bbu_elist *list)
{
	bbu_expr_free(list->expr);
	free(list);
}

void
bbu_elist_free_all(Bbu_elist *list)
{
	delayed_foreach (list)
		bbu_elist_free(list);
}

Bbu_u *
bbu_u_new(const char *dir_name, enum bbu_err *err)
{
	Bbu_u *u = palloc(u);

        pcheck_gc(u,                         err_u,    *err = BBU_ERR_MEM);
        pcheck_gc(u->dir = opendir(dir_name), err_dir,  *err = BBU_ERR_OPENDIR);
	pcheck_gc(u->load = hmap_new(0),      err_load, *err = BBU_ERR_MEM);
	pcheck_gc(u->libs = hmap_new(0),      err_libs, *err = BBU_ERR_MEM);
	u->stack = NULL;
	return u;
err_libs:
	hmap_free(u->load, NULL, NULL);
err_load:
	closedir(u->dir);
err_dir:
	free(u);
err_u:
	return NULL;
}

static void
load_free(void *key, void *storage, void *extra)
{
	(void) key;
	(void) extra;

	bbu_f_free(storage);
}

static void
libs_free(void *key, void *storage, void *extra)
{
	Bbu_dlib *lib = storage;

	(void) key;
	(void) extra;

	free(lib->path);
	dlclose(lib->handle);
	free(lib);
}

void
bbu_u_free(Bbu_u *u)
{
	closedir(u->dir);
	hmap_free(u->load, load_free, NULL);
	hmap_free(u->libs, libs_free, NULL);

	delayed_foreach(u->stack)
		free(u->stack);

	free(u);
}

int
bbu_u_add(Bbu_u *u, Bbu_f *f)
{
	return hmap_add(u->load, f->id, sizeof(f->id), f, &u->stack);
}

Bbu_f *
bbu_u_remove(Bbu_u *u, Bbu_uid id)
{
	return hmap_remove(u->load, id, sizeof(Bbu_uid), &u->stack);
}

static void
bbu_id_path(Path_str path, uuid_t id)
{
	/* first two chars '/' rest */
	uuid_unparse_lower(id, path + 1);
	path[0] = path[1];
	path[1] = path[2];
	path[2] = '/';
}

enum bbu_err
bbu_u_delete(Bbu_u *u, Bbu_uid id)
{
	int fd = dirfd(u->dir);
	Path_str path;
	Bbu_f *f;

	if (fd < 0)
		return BBU_ERR_DIRFD;

	bbu_id_path(path, id);

	if (unlinkat(fd, path, 0) < 0)
		return BBU_ERR_UNLINK;

	if ((f = bbu_u_remove(u, id)))
		bbu_f_free(f);

	return 0;
}

static int
new_fd(Bbu_u *u, Bbu_uid id, enum bbu_err *err)
{
	int dir_fd = dirfd(u->dir);
        Path_str path;
	int fd;

	if (dir_fd < 0) {
	        *err = BBU_ERR_DIRFD;
		return -1;
	}

	bbu_id_path(path, id);
	fd = openat(dir_fd, path, NEW_FD_FLAGS, NEW_FD_MODE);

	/* if we can't open the file, then the directory of the first two chars
	 * might not exist, so create that directory and try again.
	 */
	if (fd < 0) {
		char dir[3] = { path[0], path[1], '\0' };

		if (mkdirat(dir_fd, dir, DIR_MODE) < 0) {
		        *err = BBU_ERR_NEW_DIR;
			return -1;
		}

		if ((fd = openat(dir_fd, path,
				 NEW_FD_FLAGS, NEW_FD_MODE)) < 0) {
		        *err = BBU_ERR_OPEN;
			return -1;
		}
	}

	return fd;
}

static int
old_fd(Bbu_u *u, Bbu_uid id, enum bbu_err *err)
{
	int dir_fd = dirfd(u->dir);
	int fd;
	Path_str path;

	if (dir_fd < 0) {
	        *err = BBU_ERR_DIRFD;
		return -1;
	}

	bbu_id_path(path, id);

	if ((fd = openat(dir_fd, path, O_RDONLY)) < 0) {
	        *err = BBU_ERR_OPEN;
		return -1;
	}

	return fd;
}

static int
check_write(int fd, const void *buf, ssize_t size)
{
	if (write(fd, buf, size) != size)
		return -1;

	return 0;
}

static int
write_expr(int fd, Bbu_elist *list)
{
	char type_byte;
	Bbu_expr *e;
	static const char end = BBU_END;

	foreach (list) {
		e = list->expr;
		type_byte = e->type;

		if (check_write(fd, &type_byte, sizeof(type_byte)))
			return BBU_ERR_WRITE;

		switch (e->type) {
		case BBU_VAR:
			if (check_write(fd, e->i.var, sizeof(e->i.var)))
				return BBU_ERR_WRITE;

			break;
		case BBU_FA:
			if (check_write(fd, e->i.fa.f_id,
					sizeof(e->i.fa.f_id)) ||
			    write_expr(fd, e->i.fa.args))
				return BBU_ERR_WRITE;

			break;
		case BBU_BLOB:
			if (check_write(fd, &e->i.blob.size,
					sizeof(e->i.blob.size)) ||
			    check_write(fd, e->i.blob.bytes, e->i.blob.size))
				return BBU_ERR_WRITE;

			break;
		}
	}

	if (check_write(fd, &end, sizeof(end)))
		return BBU_ERR_WRITE;

	return 0;
}

enum bbu_err
bbu_u_ser(Bbu_u *u, Bbu_f *f)
{
	enum bbu_err err = 0;
	int fd = new_fd(u, f->id, &err);
	char type = f->type;
	int ret = 0;
	uint32_t lib_len;
	uint32_t sym_len;

	if (fd < 0)
		return err;

	switch (type) {
	case BBU_E:
		ret = BBU_ERR_INVALID;
	        break;
	case BBU_D:
		lib_len = strlen(f->i.d->lib->path) + 1;
		sym_len = strlen(f->i.d->sym) + 1;

		if (check_write(fd, &type,     sizeof(type)) ||
		    check_write(fd, &lib_len,  sizeof(lib_len)) ||
		    check_write(fd, &sym_len,  sizeof(sym_len)) ||
		    check_write(fd, f->i.d->lib->path, lib_len) ||
		    check_write(fd, f->i.d->sym, sym_len))
			ret = BBU_ERR_WRITE;

		f->save = 0;
	        break;
	case BBU_C:
		if (check_write(fd, &type, sizeof(type))) {
			ret = BBU_ERR_WRITE;
			break;
		}

	        if ((ret = write_expr(fd, f->i.bod)))
			break;

		f->save = 0;
	        break;
	}

	if (close(fd) != 0)
		ret = BBU_ERR_CLOSE;

	return ret;
}

static int
check_read(int fd, void *buf, ssize_t size)
{
        if (read(fd, buf, size) != size)
		return -1;

	return 0;
}

static Bbu_f *
deser_df(Bbu_u *u, Bbu_uid id, int fd, enum bbu_err *err)
{
	struct { uint32_t lib_len; uint32_t sym_len; } header;
	Bbu_f *f;

	if (check_read(fd, &header, sizeof(header))) {
	        *err = BBU_ERR_READ;
		return NULL;
	}

	char lib[header.lib_len];
	char sym[header.sym_len];

	if (check_read(fd, lib, sizeof(lib)) ||
	    check_read(fd, sym, sizeof(sym))) {
	        *err = BBU_ERR_READ;
		return NULL;
	}

	if (!(f = bbu_df(u, id, lib, sym, 0, err))) {
	        *err = BBU_ERR_MEM;
		return NULL;
	}

	return f;
}

static int
read_var(int fd, Bbu_elist **end)
{
	Bbu_vid vid;
	Bbu_expr *e;

	if (check_read(fd, vid, sizeof(vid)))
		return BBU_ERR_READ;

	pcheck(e = bbu_expr_var(vid), BBU_ERR_MEM);
	pcheck_c(*end = bbu_elist_new(e, NULL), BBU_ERR_MEM, bbu_expr_free(e));
	return 0;
}

static int
read_expr(int fd, Bbu_elist **list);

static int
read_fa(int fd, Bbu_elist **end)
{
	Bbu_uid fa_id;
	Bbu_elist *bod;
	Bbu_expr *e;
	int err;

	if (check_read(fd, fa_id, sizeof(fa_id)))
		return BBU_ERR_READ;

	if ((err = read_expr(fd, &bod)))
		return err;

	pcheck_c(e = bbu_expr_fa(fa_id, bod), BBU_ERR_MEM,
		 bbu_elist_free_all(bod));
	pcheck_c(*end = bbu_elist_new(e, NULL), BBU_ERR_MEM,
		 bbu_expr_free(e));
	return 0;
}

static int
read_blob(int fd, Bbu_elist **end)
{
	uint64_t size;
	void *bytes;
	Bbu_expr *e;


	if (check_read(fd, &size, sizeof(size)))
	        return BBU_ERR_READ;

	pcheck(bytes = malloc(size), -1);
	pcheck_c(e = bbu_expr_blob(bytes, size), BBU_ERR_MEM, free(bytes));
	pcheck_c(*end = bbu_elist_new(e, NULL),  BBU_ERR_MEM,
		 bbu_expr_free(e));
	return 0;
}

static int
read_expr(int fd, Bbu_elist **list)
{
	Bbu_elist *head = NULL;
	Bbu_elist **end = &head;
	int err_val;
	char type;

	for (;; end = NEXT_REF(*end, Bbu_elist)) {
		if (check_read(fd, &type, sizeof(type))) {
			err_val = BBU_ERR_READ;
			goto err;
		}


		switch (type) {
		case BBU_END:
			goto out;
		case BBU_VAR:
			if ((err_val = read_var(fd, end)))
				goto err;

			break;
		case BBU_FA:
			if ((err_val = read_fa(fd, end)))
				goto err;

			break;
		case BBU_BLOB:
			if ((err_val = read_blob(fd, end)))
				goto err;

			break;
		}
	}

out:
	*list = head;
	return 0;
err:
	bbu_elist_free_all(head);
	return err_val;
}

static Bbu_f *
deser_cf(Bbu_uid id, int fd, enum bbu_err *err)
{
	Bbu_elist *bod;
	Bbu_f *f;

	if ((*err = read_expr(fd, &bod)))
		return NULL;

	if (!(f = bbu_cf(id, bod, 0))) {
	        *err = BBU_ERR_MEM;
		bbu_elist_free_all(bod);
		return NULL;
	}

	return f;
}

Bbu_f *
bbu_u_deser(Bbu_u *u, Bbu_uid id, enum bbu_err *err)
{
	int fd = old_fd(u, id, err);
	char type;
	Bbu_f *f = NULL;

	if (fd < 0)
	        goto err_fd;

	if (check_read(fd, &type, sizeof(type))) {
	        *err = BBU_ERR_READ;
		goto err_type;
	}

	switch (type) {
	case BBU_D:
		f = deser_df(u, id, fd, err);
		break;
	case BBU_C:
		f = deser_cf(id, fd, err);
		break;
	}

	/* These closes do not need to be checked since nothing is written to
	 * the file and thus cannot fail.
	 */
	close(fd);
	return f;
err_type:
	close(fd);
err_fd:
	return NULL;
}

Bbu_f *
bbu_u_get(Bbu_u *u, Bbu_uid id, enum bbu_err *err)
{
	Bbu_f *f = hmap_get(u->load, id, sizeof(Bbu_uid));

	if (f)
		return f;

	pcheck(f = bbu_u_deser(u, id, err), NULL);

	if (bbu_u_add(u, f)) {
	        *err = BBU_ERR_MEM;
		bbu_f_free(f);
		return NULL;
	}

	return f;
}

Bbu_f *
bbu_ef(Bbu_uid id, Bbu_bf bf)
{
	Bbu_f *f = palloc(f);

	pcheck(f, NULL);
	id_gen(f->id, id);
	f->type = BBU_E;
	f->save = 0;
	f->i.e = bf;
	return f;
}

static Bbu_dlib *
dlib_add(Bbu_u *u, const char *lib, uint32_t len, enum bbu_err *err)
{
	Bbu_dlib *dlib = palloc(dlib);

	pcheck_gc(dlib,                    err_dlib, *err = BBU_ERR_MEM);
	pcheck_gc(dlib->path = malloc(len), err_path, *err = BBU_ERR_MEM);
	strcpy(dlib->path, lib);
	pcheck_gc(dlib->handle = dlopen(lib, RTLD_LAZY), err_handle,
		  *err = BBU_ERR_DLOPEN);

	if (hmap_add(u->libs, dlib->path, len, dlib, &u->stack)) {
	        *err = BBU_ERR_MEM;
		goto err_hmap;
	}

	return dlib;
err_hmap:
	/* We have an error anyway, so with failure here nothing can be done */
	dlclose(dlib->handle);
err_handle:
	free(dlib->path);
err_path:
	free(dlib);
err_dlib:
	return NULL;
}

static Bbu_dlib *
dlib_get_or_add(Bbu_u *u, const char *lib, enum bbu_err *err)
{
        uint32_t len = strlen(lib) + 1;
	Bbu_dlib *dlib = hmap_get(u->libs, lib, len);

	if (!dlib)
		dlib = dlib_add(u, lib, len, err);

	return dlib;
}

Bbu_f *
bbu_df(Bbu_u *u, Bbu_uid id, const char *lib, const char *sym, int save,
       enum bbu_err *err)
{
	Bbu_dlib *dlib = dlib_get_or_add(u, lib, err);
	uint32_t sym_len = strlen(sym) + 1;
	Bbu_df *df;
	Bbu_f *f;

	pcheck(dlib, NULL);
	pcheck_gc(df = palloc(df), err_df, *err = BBU_ERR_MEM);
	pcheck_gc(f  = palloc(f),  err_f,  *err = BBU_ERR_MEM);
	/* POSIX.1-2003 workaround */
	pcheck_gc(*(void **) &df->bf = dlsym(dlib->handle, sym),
		  err_bf, *err = BBU_ERR_DLSYM);
	pcheck_gc(df->sym = malloc(sym_len), err_sym, *err = BBU_ERR_MEM);
	strcpy(df->sym, sym);
	df->lib = dlib;
	id_gen(f->id, id);
	f->type = BBU_D;
	f->save = save;
	f->i.d  = df;
	return f;
err_sym:
	free(df->sym);
err_bf:
	free(f);
err_f:
	free(df);
err_df:
	return NULL;
}

Bbu_f *
bbu_cf(Bbu_uid id, Bbu_elist *bod, int save)
{
	Bbu_f *f = palloc(f);

	pcheck(f, NULL);
	id_gen(f->id, id);
	f->type = BBU_C;
	f->save = save;
	f->i.bod = bod;
	return f;
}

void
bbu_f_new_id(Bbu_f *f, Bbu_uid id)
{
	id_gen(f->id, id);
}

void
bbu_f_free(Bbu_f *f)
{
	switch (f->type) {
	case BBU_E:
		break;
	case BBU_D:
		free(f->i.d->sym);
		free(f->i.d);
		break;
	case BBU_C:
		bbu_elist_free_all(f->i.bod);
		break;
	}

	free(f);
}

